/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package io.milton.http.http11;

import io.milton.http.*;
import io.milton.http.Response.Status;
import io.milton.http.entity.BufferingGetableResourceEntity;
import io.milton.http.entity.GetableResourceEntity;
import io.milton.http.entity.PartialEntity;
import io.milton.http.exceptions.BadRequestException;
import io.milton.http.exceptions.NotAuthorizedException;
import io.milton.http.exceptions.NotFoundException;
import io.milton.resource.BufferingControlResource;
import io.milton.resource.GetableResource;
import io.milton.resource.Resource;
import io.milton.servlet.ServletRequest;
import io.milton.servlet.ServletResponse;
import jakarta.servlet.ServletException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.*;

/**
 *
 */
public class DefaultHttp11ResponseHandler implements Http11ResponseHandler, Bufferable {

    private static final Logger log = LoggerFactory.getLogger(DefaultHttp11ResponseHandler.class);
    private static final String miltonVerson;

    static {
        Properties props = new Properties();
        try {
            props.load(DefaultHttp11ResponseHandler.class.getResourceAsStream("/milton.properties"));
        } catch (IOException ex) {
            log.warn("Failed lot load milton properties file", ex);
        }
        miltonVerson = props.getProperty("milton.version");
    }

    public enum BUFFERING {

        always,
        never,
        whenNeeded
    }

    private final AuthenticationService authenticationService;
    private final ETagGenerator eTagGenerator;
    private final ContentGenerator contentGenerator;
    private CacheControlHelper cacheControlHelper = new DefaultCacheControlHelper();
    private int maxMemorySize = 100000;
    private BUFFERING buffering;
    private String multipartBoundary = UUID.randomUUID().toString();

    public DefaultHttp11ResponseHandler(AuthenticationService authenticationService, ETagGenerator eTagGenerator, ContentGenerator contentGenerator) {
        this.authenticationService = authenticationService;
        this.eTagGenerator = eTagGenerator;
        this.contentGenerator = contentGenerator;
    }

    /**
     * Defaults to io.milton.http.http11.DefaultCacheControlHelper
     *
     * @return
     */
    public CacheControlHelper getCacheControlHelper() {
        return cacheControlHelper;
    }

    public void setCacheControlHelper(CacheControlHelper cacheControlHelper) {
        this.cacheControlHelper = cacheControlHelper;
    }

    @Override
    public String generateEtag(Resource r) {
        return eTagGenerator.generateEtag(r);
    }

    @Override
    public void respondWithOptions(Resource resource, Response response, Request request, List<String> methodsAllowed) {
        setRespondCommonHeaders(response, resource, Status.SC_OK, request.getAuthorization());
        response.setAllowHeader(methodsAllowed);
        response.setContentLengthHeader((long) 0); // Note that setting content length must be done last for tomcat5
    }

    @Override
    public void respondNotFound(Response response, Request request) {
        response.setStatus(Response.Status.SC_NOT_FOUND);
        response.setContentTypeHeader("text/html");
        contentGenerator.generate(null, request, response, Status.SC_NOT_FOUND);
    }

    @Override
    public void respondUnauthorised(Resource resource, Response response, Request request) {
        if (authenticationService.canUseExternalAuth(resource, request)) {
            log.info("respondUnauthorised: use external authentication");
            initiateExternalAuth(resource, request, response);
        } else {
            Auth auth = request.getAuthorization();
            if (auth == null || auth.getTag() == null) {
                log.info("respondUnauthorised: no authenticated user, so return status: " + Response.Status.SC_UNAUTHORIZED);
                response.setStatus(Response.Status.SC_UNAUTHORIZED);
                List<String> challenges = authenticationService.getChallenges(resource, request);
                response.setAuthenticateHeader(challenges);

            } else {
                log.info("respondUnauthorised: request has an authenticated user, so return status: " + Response.Status.SC_FORBIDDEN);
                response.setStatus(Response.Status.SC_FORBIDDEN);

            }
        }
    }

    @Override
    public void respondMethodNotImplemented(Resource resource, Response response, Request request) {
        response.setStatus(Response.Status.SC_NOT_IMPLEMENTED);
        contentGenerator.generate(resource, request, response, Status.SC_NOT_IMPLEMENTED);
    }

    @Override
    public void respondMethodNotAllowed(Resource res, Response response, Request request) {
        log.debug("method not allowed. handler: " + this.getClass().getName() + " resource: " + res.getClass().getName());
        response.setStatus(Response.Status.SC_METHOD_NOT_ALLOWED);
        contentGenerator.generate(res, request, response, Status.SC_METHOD_NOT_ALLOWED);
    }

    /**
     * @param resource
     * @param response
     * @param request
     * @param message  - optional message to output in the body content
     */
    @Override
    public void respondConflict(Resource resource, Response response, Request request, String message) {
        log.debug("respondConflict");
        response.setStatus(Response.Status.SC_CONFLICT);
        contentGenerator.generate(resource, request, response, Status.SC_CONFLICT);
    }

    @Override
    public void respondServerError(Request request, Response response, String reason) {
        response.setStatus(Status.SC_INTERNAL_SERVER_ERROR);
        contentGenerator.generate(null, request, response, Status.SC_INTERNAL_SERVER_ERROR);
    }

    @Override
    public void respondRedirect(Response response, Request request, String redirectUrl) {
        if (redirectUrl == null) {
            throw new NullPointerException("redirectUrl cannot be null");
        }
        log.trace("respondRedirect");
        // delegate to the response, because this can be server dependent
        try {
            ServletRequest.getRequest().getRequestDispatcher(redirectUrl.substring(redirectUrl.lastIndexOf("/"))).forward(ServletRequest.getRequest(), ServletResponse.getResponse());
        } catch (ServletException e) {


        } catch (IOException e) {
            e.printStackTrace();
        }

//		response.sendRedirect(redirectUrl);
//        response.setStatus(Response.Status.SC_MOVED_TEMPORARILY);
//        response.setLocationHeader(redirectUrl);
    }

    @Override
    public void respondExpectationFailed(Response response, Request request) {
        response.setStatus(Response.Status.SC_EXPECTATION_FAILED);
    }

    @Override
    public void respondCreated(Resource resource, Response response, Request request) {
//        log.debug( "respondCreated" );
        setRespondCommonHeaders(response, resource, Status.SC_CREATED, request.getAuthorization());
    }

    @Override
    public void respondNoContent(Resource resource, Response response, Request request) {
//        log.debug( "respondNoContent" );
        //response.setStatus(Response.Status.SC_OK);
        // see comments in http://www.ettrema.com:8080/browse/MIL-87
        setRespondCommonHeaders(response, resource, Status.SC_NO_CONTENT, request.getAuthorization());
    }

    @Override
    public void respondPartialContent(GetableResource resource, Response response, Request request, Map<String, String> params, Range range) throws NotAuthorizedException, BadRequestException, NotFoundException {
        log.debug("respondPartialContent: " + range.getStart() + " - " + range.getFinish());
        response.setStatus(Response.Status.SC_PARTIAL_CONTENT);
        long st = range.getStart() == null ? 0 : range.getStart();
        long fn;
        Long cl = resource.getContentLength();
        if (range.getFinish() == null) {
            if (cl != null) {
                fn = cl - 1; // position is one less then length
            } else {
                log.warn("Couldnt calculate range end position because the resource is not reporting a content length, and no end position was requested by the client: " + resource.getName() + " - " + resource.getClass());
                fn = -1;
            }
        } else {
            if (cl != null && cl < range.getFinish()) {
                fn = cl - 1;
            } else if (cl == null) {
                log.warn("Couldnt calculate range end position because the resource is not reporting a content length, and no end position was requested by the client: " + resource.getName() + " - " + resource.getClass());
                fn = -1;
            } else {
                fn = range.getFinish();
            }
        }
        response.setContentRangeHeader(st, fn, cl);
        long contentLength = fn - st + 1;
        response.setDateHeader(new Date());
        String etag = eTagGenerator.generateEtag(resource);
        if (etag != null) {
            response.setEtag(etag);
        }
        //String acc = request.getAcceptHeader();
//		String ct = resource.getContentType(acc);
//		if (ct != null) {
//			response.setContentTypeHeader(ct);
//		}
        response.setContentLengthHeader(contentLength);
        response.setEntity(new GetableResourceEntity(resource, range, params, null));
    }

    /**
     * Send a partial content response with multiple ranges
     *
     * @param resource
     * @param response
     * @param request
     * @param params
     * @param ranges
     * @throws NotAuthorizedException
     * @throws BadRequestException
     * @throws NotFoundException
     */
    @Override
    public void respondPartialContent(GetableResource resource, Response response, Request request, Map<String, String> params, List<Range> ranges) throws NotAuthorizedException, BadRequestException, NotFoundException {
        log.debug("respondPartialContent - multiple ranges");
        response.setStatus(Response.Status.SC_PARTIAL_CONTENT);
        response.setAcceptRanges("bytes");
        response.setDateHeader(new Date());
        String etag = eTagGenerator.generateEtag(resource);
        if (etag != null) {
            response.setEtag(etag);
        }
        response.setContentTypeHeader("multipart/byteranges; boundary=" + multipartBoundary);
        String acc = request.getAcceptHeader();
        String ct = resource.getContentType(acc);

        response.setEntity(
                new PartialEntity(resource, ranges, params, ct, multipartBoundary)
        );

    }

    @Override
    public void respondHead(Resource resource, Response response, Request request) {
        //setRespondContentCommonHeaders(response, resource, Response.Status.SC_NO_CONTENT, request.getAuthorization());
        setRespondContentCommonHeaders(response, resource, Response.Status.SC_OK, request.getAuthorization());
        if (!(resource instanceof GetableResource)) {
            return;
        }
        GetableResource gr = (GetableResource) resource;
        String acc = request.getAcceptHeader();
        String ct = gr.getContentType(acc);
        if (ct != null) {
            ct = pickBestContentType(ct);
            if (ct != null) {
                response.setContentTypeHeader(ct);
            }
        }
        Long contentLength = gr.getContentLength();
        if (contentLength != null) {
            response.setContentLengthHeader(contentLength);
        } else {
            log.trace("No content length is available for HEAD request");
        }
    }

    @Override
    public void respondContent(Resource resource, Response response, Request request, Map<String, String> params) throws NotAuthorizedException, BadRequestException, NotFoundException {
        log.debug("respondContent: " + resource.getClass());
        Auth auth = request.getAuthorization();
        setRespondContentCommonHeaders(response, resource, auth);
        if (resource instanceof GetableResource) {
            GetableResource gr = (GetableResource) resource;
            String acc = request.getAcceptHeader();
            String ct = gr.getContentType(acc);
            if (ct != null) {
                ct = pickBestContentType(ct);
                response.setContentTypeHeader(ct);
            }
            cacheControlHelper.setCacheControl(gr, response, request.getAuthorization());

            Long contentLength = gr.getContentLength();
            Boolean doBuffering = null;
            if (resource instanceof BufferingControlResource) {
                BufferingControlResource bcr = (BufferingControlResource) resource;
                doBuffering = bcr.isBufferingRequired();
            }
            if (doBuffering == null) {
                if (buffering == null || buffering == BUFFERING.whenNeeded) {
                    doBuffering = (contentLength == null); // if no content length then we buffer content to find content length
                } else {
                    doBuffering = (buffering == BUFFERING.always); // if not null or whenNeeded then buffering is explicitly enabled or disabled
                }
            }
            if (!doBuffering) {
                log.trace("sending content with known content length: " + contentLength);
                if (contentLength != null) {
                    response.setContentLengthHeader(contentLength);
                }
                response.setEntity(new GetableResourceEntity(gr, params, ct));
            } else {
                BufferingGetableResourceEntity e = new BufferingGetableResourceEntity(gr, params, ct, contentLength, getMaxMemorySize());
                response.setEntity(e);
            }
        }
    }

    @Override
    public void respondNotModified(GetableResource resource, Response response, Request request) {
        log.trace("respondNotModified");
        response.setStatus(Response.Status.SC_NOT_MODIFIED);
        response.setDateHeader(new Date());
        String etag = eTagGenerator.generateEtag(resource);
        if (etag != null) {
            response.setEtag(etag);
        }

        // Note that we use a simpler modified date handling here then when
        // responding with content, because in a not-modified situation the
        // modified date MUST be that of the actual resource
        Date modDate = resource.getModifiedDate();
        response.setLastModifiedHeader(modDate);

        cacheControlHelper.setCacheControl(resource, response, request.getAuthorization());
    }

    protected void setRespondContentCommonHeaders(Response response, Resource resource, Auth auth) {
        setRespondContentCommonHeaders(response, resource, Response.Status.SC_OK, auth);
    }

    protected void setRespondContentCommonHeaders(Response response, Resource resource, Response.Status status, Auth auth) {
        setRespondCommonHeaders(response, resource, status, auth);
        setModifiedDate(response, resource, auth);
    }

    protected void setRespondCommonHeaders(Response response, Resource resource, Response.Status status, Auth auth) {
        response.setStatus(status);
        response.setNonStandardHeader("Server", "milton.io-" + miltonVerson);
        response.setDateHeader(new Date());
        response.setNonStandardHeader("Accept-Ranges", "bytes");
        String etag = eTagGenerator.generateEtag(resource);
        if (etag != null) {
            response.setEtag(etag);
        }
    }

    /**
     * The modified date response header is used by the client for content
     * caching. It seems obvious that if we have a modified date on the resource
     * we should set it. BUT, because of the interaction with max-age we should
     * always set it to the current date if we have max-age The problem, is that
     * if we find that a condition GET has an expired mod-date (based on maxAge)
     * then we want to respond with content (even if our mod-date hasnt changed.
     * But if we use the actual mod-date in that case, then the browser will
     * continue to use the old mod-date, so will forever more respond with
     * content. So we send a mod-date of now to ensure that future requests will
     * be given a 304 not modified.*
     *
     * @param response
     * @param resource
     * @param auth
     */
    public static void setModifiedDate(Response response, Resource resource, Auth auth) {
        Date modDate = resource.getModifiedDate();
        if (modDate != null) {
            // HACH - see if this helps IE
            response.setLastModifiedHeader(modDate);
//            if (resource instanceof GetableResource) {
//                GetableResource gr = (GetableResource) resource;
//                Long maxAge = gr.getMaxAgeSeconds(auth);
//                if (maxAge != null && maxAge > 0) {
//                    log.trace("setModifiedDate: has a modified date and a positive maxAge, so adjust modDate");
//                    long tm = System.currentTimeMillis() - 60000; // modified 1 minute ago
//                    modDate = new Date(tm); // have max-age, so use current date
//                }
//            }
//            response.setLastModifiedHeader(modDate);
        }
    }

    @Override
    public void respondBadRequest(Resource resource, Response response, Request request) {
        response.setStatus(Response.Status.SC_BAD_REQUEST);
    }

    @Override
    public void respondForbidden(Resource resource, Response response, Request request) {
        response.setStatus(Response.Status.SC_FORBIDDEN);
    }

    @Override
    public void respondDeleteFailed(Request request, Response response, Resource resource, Status status) {
        response.setStatus(status);
    }

    @Override
    public void respondPreconditionFailed(Request request, Response response, Resource resource) {
        response.setStatus(Status.SC_PRECONDITION_FAILED);
    }

    public AuthenticationService getAuthenticationService() {
        return authenticationService;
    }

    /**
     * Maximum size of data to hold in memory per request when buffering output
     * data.
     *
     * @return
     */
    public int getMaxMemorySize() {
        return maxMemorySize;
    }

    public void setMaxMemorySize(int maxMemorySize) {
        this.maxMemorySize = maxMemorySize;
    }

    @Override
    public BUFFERING getBuffering() {
        return buffering;
    }

    @Override
    public void setBuffering(BUFFERING buffering) {
        this.buffering = buffering;
    }

    /**
     * Sometimes we'll get a content type list, such as image/jpeg,image/pjpeg
     * <p>
     * In this case we should pick the first in the list
     *
     * @param ct
     * @return
     */
    private String pickBestContentType(String ct) {
        if (ct == null) {
            return null;
        } else if (ct.contains(",")) {
            return ct.split(",")[0];
        } else {
            return ct;
        }
    }

    public void initiateExternalAuth(Resource resource, Request request, Response response) {
        ExternalIdentityProvider eip = getSelectedIP(request);
        if (eip == null) {
            // means that the user needs to select an identity provider, so generate appropriate page
        } else {
            eip.initiateExternalAuth(resource, request, response);
        }
    }

    private ExternalIdentityProvider getSelectedIP(Request request) {
        List<ExternalIdentityProvider> list = authenticationService.getExternalIdentityProviders();
        if (list.size() == 1) {
            return list.get(0);
        } else {
            String ipName = request.getParams().get("_ip");
            if (ipName != null && ipName.length() > 0) {
                for (ExternalIdentityProvider eip : list) {
                    if (ipName.equals(eip.getName())) {
                        return eip;
                    }

                }
            }
            return null;
        }
    }

    public ContentGenerator getContentGenerator() {
        return contentGenerator;
    }

    public String getMultipartBoundary() {
        return multipartBoundary;
    }

    public void setMultipartBoundary(String multipartBoundary) {
        this.multipartBoundary = multipartBoundary;
    }
}
